#!/usr/bin/python
#
# Scaleway database backups management module
#
# Copyright (C) 2020 Guillaume Rodriguez (g.rodriguez@opendecide.com).
#
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations


DOCUMENTATION = r"""
module: scaleway_database_backup
short_description: Scaleway database backups management module
version_added: 1.2.0
author: Guillaume Rodriguez (@guillaume_ro_fr)
description:
  - This module manages database backups on Scaleway account U(https://developer.scaleway.com).
extends_documentation_fragment:
  - community.general.scaleway
  - community.general.attributes
  - community.general.scaleway.actiongroup_scaleway
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
  action_group:
    version_added: 11.3.0

options:
  state:
    description:
      - Indicate desired state of the database backup.
      - V(present) creates a backup.
      - V(absent) deletes the backup.
      - V(exported) creates a download link for the backup.
      - V(restored) restores the backup to a new database.
    type: str
    default: present
    choices:
      - present
      - absent
      - exported
      - restored

  region:
    description:
      - Scaleway region to use (for example V(fr-par)).
    type: str
    required: true
    choices:
      - fr-par
      - nl-ams
      - pl-waw

  id:
    description:
      - UUID used to identify the database backup.
      - Required for V(absent), V(exported) and V(restored) states.
    type: str

  name:
    description:
      - Name used to identify the database backup.
      - Required for V(present) state.
      - Ignored when O(state=absent), O(state=exported) or O(state=restored).
    type: str

  database_name:
    description:
      - Name used to identify the database.
      - Required for V(present) and V(restored) states.
      - Ignored when O(state=absent) or O(state=exported).
    type: str

  instance_id:
    description:
      - UUID of the instance associated to the database backup.
      - Required for V(present) and V(restored) states.
      - Ignored when O(state=absent) or O(state=exported).
    type: str

  expires_at:
    description:
      - Expiration datetime of the database backup (ISO 8601 format).
      - Ignored when O(state=absent), O(state=exported) or O(state=restored).
    type: str

  wait:
    description:
      - Wait for the instance to reach its desired state before returning.
    type: bool
    default: false

  wait_timeout:
    description:
      - Time to wait for the backup to reach the expected state.
    type: int
    default: 300

  wait_sleep_time:
    description:
      - Time to wait before every attempt to check the state of the backup.
    type: int
    default: 3
"""

EXAMPLES = r"""
- name: Create a backup
  community.general.scaleway_database_backup:
    name: 'my_backup'
    state: present
    region: 'fr-par'
    database_name: 'my-database'
    instance_id: '50968a80-2909-4e5c-b1af-a2e19860dddb'

- name: Export a backup
  community.general.scaleway_database_backup:
    id: '6ef1125a-037e-494f-a911-6d9c49a51691'
    state: exported
    region: 'fr-par'

- name: Restore a backup
  community.general.scaleway_database_backup:
    id: '6ef1125a-037e-494f-a911-6d9c49a51691'
    state: restored
    region: 'fr-par'
    database_name: 'my-new-database'
    instance_id: '50968a80-2909-4e5c-b1af-a2e19860dddb'

- name: Remove a backup
  community.general.scaleway_database_backup:
    id: '6ef1125a-037e-494f-a911-6d9c49a51691'
    state: absent
    region: 'fr-par'
"""

RETURN = r"""
metadata:
  description: Backup metadata.
  returned: when O(state=present), O(state=exported), or O(state=restored)
  type: dict
  sample:
    {
      "metadata": {
        "created_at": "2020-08-06T12:42:05.631049Z",
        "database_name": "my-database",
        "download_url": null,
        "download_url_expires_at": null,
        "expires_at": null,
        "id": "a15297bd-0c4a-4b4f-8fbb-b36a35b7eb07",
        "instance_id": "617be32e-6497-4ed7-b4c7-0ee5a81edf49",
        "instance_name": "my-instance",
        "name": "backup_name",
        "region": "fr-par",
        "size": 600000,
        "status": "ready",
        "updated_at": "2020-08-06T12:42:10.581649Z"
      }
    }
"""

import datetime
import time

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.datetime import (
    now,
)
from ansible_collections.community.general.plugins.module_utils.scaleway import (
    Scaleway,
    scaleway_argument_spec,
    SCALEWAY_REGIONS,
)

stable_states = (
    "ready",
    "deleting",
)


def wait_to_complete_state_transition(module, account_api, backup=None):
    wait_timeout = module.params["wait_timeout"]
    wait_sleep_time = module.params["wait_sleep_time"]

    if backup is None or backup["status"] in stable_states:
        return backup

    start = now()
    end = start + datetime.timedelta(seconds=wait_timeout)
    while now() < end:
        module.debug("We are going to wait for the backup to finish its transition")

        response = account_api.get(f"/rdb/v1/regions/{module.params.get('region')}/backups/{backup['id']}")
        if not response.ok:
            module.fail_json(msg=f"Error getting backup [{response.status_code}: {response.json}]")
            break
        response_json = response.json

        if response_json["status"] in stable_states:
            module.debug("It seems that the backup is not in transition anymore.")
            module.debug(f"Backup in state: {response_json['status']}")
            return response_json
        time.sleep(wait_sleep_time)
    else:
        module.fail_json(msg="Backup takes too long to finish its transition")


def present_strategy(module, account_api, backup):
    name = module.params["name"]
    database_name = module.params["database_name"]
    instance_id = module.params["instance_id"]
    expiration_date = module.params["expires_at"]

    if backup is not None:
        if (backup["name"] == name or name is None) and (
            backup["expires_at"] == expiration_date or expiration_date is None
        ):
            wait_to_complete_state_transition(module, account_api, backup)
            module.exit_json(changed=False)

        if module.check_mode:
            module.exit_json(changed=True)

        payload = {}
        if name is not None:
            payload["name"] = name
        if expiration_date is not None:
            payload["expires_at"] = expiration_date

        response = account_api.patch(f"/rdb/v1/regions/{module.params.get('region')}/backups/{backup['id']}", payload)
        if response.ok:
            result = wait_to_complete_state_transition(module, account_api, response.json)
            module.exit_json(changed=True, metadata=result)

        module.fail_json(msg=f"Error modifying backup [{response.status_code}: {response.json}]")

    if module.check_mode:
        module.exit_json(changed=True)

    payload = {"name": name, "database_name": database_name, "instance_id": instance_id}
    if expiration_date is not None:
        payload["expires_at"] = expiration_date

    response = account_api.post(f"/rdb/v1/regions/{module.params.get('region')}/backups", payload)

    if response.ok:
        result = wait_to_complete_state_transition(module, account_api, response.json)
        module.exit_json(changed=True, metadata=result)

    module.fail_json(msg=f"Error creating backup [{response.status_code}: {response.json}]")


def absent_strategy(module, account_api, backup):
    if backup is None:
        module.exit_json(changed=False)

    if module.check_mode:
        module.exit_json(changed=True)

    response = account_api.delete(f"/rdb/v1/regions/{module.params.get('region')}/backups/{backup['id']}")
    if response.ok:
        result = wait_to_complete_state_transition(module, account_api, response.json)
        module.exit_json(changed=True, metadata=result)

    module.fail_json(msg=f"Error deleting backup [{response.status_code}: {response.json}]")


def exported_strategy(module, account_api, backup):
    if backup is None:
        module.fail_json(msg=f'Backup "{module.params["id"]}" not found')

    if backup["download_url"] is not None:
        module.exit_json(changed=False, metadata=backup)

    if module.check_mode:
        module.exit_json(changed=True)

    backup = wait_to_complete_state_transition(module, account_api, backup)
    response = account_api.post(f"/rdb/v1/regions/{module.params.get('region')}/backups/{backup['id']}/export", {})

    if response.ok:
        result = wait_to_complete_state_transition(module, account_api, response.json)
        module.exit_json(changed=True, metadata=result)

    module.fail_json(msg=f"Error exporting backup [{response.status_code}: {response.json}]")


def restored_strategy(module, account_api, backup):
    if backup is None:
        module.fail_json(msg=f'Backup "{module.params["id"]}" not found')

    database_name = module.params["database_name"]
    instance_id = module.params["instance_id"]

    if module.check_mode:
        module.exit_json(changed=True)

    backup = wait_to_complete_state_transition(module, account_api, backup)

    payload = {"database_name": database_name, "instance_id": instance_id}
    response = account_api.post(
        f"/rdb/v1/regions/{module.params.get('region')}/backups/{backup['id']}/restore", payload
    )

    if response.ok:
        result = wait_to_complete_state_transition(module, account_api, response.json)
        module.exit_json(changed=True, metadata=result)

    module.fail_json(msg=f"Error restoring backup [{response.status_code}: {response.json}]")


state_strategy = {
    "present": present_strategy,
    "absent": absent_strategy,
    "exported": exported_strategy,
    "restored": restored_strategy,
}


def core(module):
    state = module.params["state"]
    backup_id = module.params["id"]

    account_api = Scaleway(module)

    if backup_id is None:
        backup_by_id = None
    else:
        response = account_api.get(f"/rdb/v1/regions/{module.params.get('region')}/backups/{backup_id}")
        status_code = response.status_code
        backup_json = response.json
        backup_by_id = None
        if status_code == 404:
            backup_by_id = None
        elif response.ok:
            backup_by_id = backup_json
        else:
            module.fail_json(msg=f"Error getting backup [{status_code}: {response.json['message']}]")

    state_strategy[state](module, account_api, backup_by_id)


def main():
    argument_spec = scaleway_argument_spec()
    argument_spec.update(
        dict(
            state=dict(default="present", choices=["absent", "present", "exported", "restored"]),
            region=dict(required=True, choices=SCALEWAY_REGIONS),
            id=dict(),
            name=dict(type="str"),
            database_name=dict(),
            instance_id=dict(),
            expires_at=dict(),
            wait=dict(type="bool", default=False),
            wait_timeout=dict(type="int", default=300),
            wait_sleep_time=dict(type="int", default=3),
        )
    )
    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_together=[
            ["database_name", "instance_id"],
        ],
        required_if=[
            ["state", "present", ["name", "database_name", "instance_id"]],
            ["state", "absent", ["id"]],
            ["state", "exported", ["id"]],
            ["state", "restored", ["id", "database_name", "instance_id"]],
        ],
    )

    core(module)


if __name__ == "__main__":
    main()
